﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.IO;

using AnB.Common;

namespace AnB.Net
{
    public class Packet
    {
        public Packet(ICmdHelper cmdHelper = null)
        {
            mCmdHelper = cmdHelper;
        }

        public interface ICmdHelper
        {
            CmdHandler GetCmdHandler(ushort cmd);
            CmdHandler GetCmdHandler(string cmd);
        }
        private ICmdHelper mCmdHelper;

        public const int HEAD_LEN = 6;
        public enum Flags
        {
            BodyTypeBits = 0x0f,
            Response     = 0x10,
            NeedResponse = 0x20,
            Compress     = 0x40,
        }
        public enum BodyType
        {
            None,
            Binary,
            Boolean,
            Integer,
            UInteger,
            String,
            ProtoBuf,
        }

        public ushort Id;
        public ushort Cmd;
        public BodyType Type;
        public bool Response;
        public bool NeedResponse;
        public bool Compress;
        public Error.Code ErrorCode;
        public object Data;

        public static BodyType GetBodyType(Type type)
        {
            if (type == null || type == typeof(void))
            {
                return BodyType.None;
            }
            if (type == typeof(byte[]))
            {
                return BodyType.Binary;
            }
            if (type == typeof(bool))
            {
                return BodyType.Boolean;
            }
            if (type == typeof(char) || type == typeof(short) || type == typeof(int) || type == typeof(long))
            {
                return BodyType.Integer;
            }
            if (type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong))
            {
                return BodyType.UInteger;
            }
            if (type == typeof(string))
            {
                return BodyType.String;
            }
            if (type.GetInterfaces().Contains(typeof(ProtoBuf.IExtensible)))
            {
                return BodyType.ProtoBuf;
            }
            throw new Exception("no BodyType for " + type.Name);
        }

        public List<ArraySegment<byte>> Serialize()
        {
            byte[] head = new byte[HEAD_LEN];
            byte flags = (byte)((byte)Type & (byte)Flags.BodyTypeBits);
            if (Response)
            {
                flags |= (byte)Flags.Response;
            }
            if (NeedResponse)
            {
                flags |= (byte)Flags.NeedResponse;
            }
            head[0] = (byte)((Id  >> 8) & 0xff);
            head[1] = (byte)((Id      ) & 0xff);
            head[2] = (byte)((Cmd >> 8) & 0xff);
            head[3] = (byte)((Cmd     ) & 0xff);
            head[4] = (byte)((flags   ) & 0xff);
            head[5] = (byte)ErrorCode;

            byte[] body;
            switch (Type)
            {
                case BodyType.None:
                    {
                        body = null;
                    }
                    break;
                case BodyType.Binary:
                    {
                        body = (byte[])Data;
                    }
                    break;
                case BodyType.Boolean:
                    {
                        body = new byte[] { (bool)Data ? (byte)1 : (byte)0 };
                    }
                    break;
                case BodyType.Integer:
                    {
                        if (Data is long)
                        {
                            long data = (long)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else if (Data is int)
                        {
                            int data = (int)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else if (Data is short)
                        {
                            short data = (short)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else if (Data is char)
                        {
                            char data = (char)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else
                        {
                            string s = string.Format("invalid Integer type {0}", Data.GetType().Name);
                            throw new Exception(s);
                        }
                        if (BitConverter.IsLittleEndian)
                        {
                            Array.Reverse(body);
                        }
                    }
                    break;
                case BodyType.UInteger:
                    {
                        if (Data is ulong)
                        {
                            ulong data = (ulong)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else if (Data is uint)
                        {
                            uint data = (uint)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else if (Data is ushort)
                        {
                            ushort data = (ushort)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else if (Data is byte)
                        {
                            byte data = (byte)Data;
                            body = BitConverter.GetBytes(data);
                        }
                        else
                        {
                            string s = string.Format("invalid Integer type {0}", Data.GetType().Name);
                            throw new Exception(s);
                        }
                        if (BitConverter.IsLittleEndian)
                        {
                            Array.Reverse(body);
                        }
                    }
                    break;
                case BodyType.String:
                    {
                        body = Encoding.UTF8.GetBytes((string)Data);
                    }
                    break;
                case BodyType.ProtoBuf:
                    {
                        body = PB.Serialize(Data);
                    }
                    break;
                default:
                    {
                        throw new Exception("invalid body type");
                    }
                    //break;
            }
            var segments = new List<ArraySegment<byte>>()
            {
                new ArraySegment<byte>(head),
            };
            if (body != null && body.Length > 0)
            {
                segments.Add(new ArraySegment<byte>(body));
            }
            return segments;
        }
        public void DeserializeHead(byte[] segment, int index, int count)
        {
            if (count < HEAD_LEN)
            {
                string s = string.Format("invalid head length {0} < {1}", count, HEAD_LEN);
                throw new Exception(s);
            }
            Id = (ushort)(
                ((segment[index + 0] & 0xff) <<  8) +
                ((segment[index + 1] & 0xff)      )
                );
            Cmd = (ushort)(
                ((segment[index + 2] & 0xff) <<  8) +
                ((segment[index + 3] & 0xff)      )
                );

            byte flags = segment[index + 4];
            Type = (BodyType)(flags & (byte)Flags.BodyTypeBits);
            Response = (flags & (byte)Flags.Response) != 0;
            NeedResponse = (flags & (byte)Flags.NeedResponse) != 0;
            Compress = (flags & (byte)Flags.Compress) != 0;

            ErrorCode = (Error.Code)segment[index + 5];
        }

        public void DeserialzeBody(byte[] segment, int index, int count)
        {
            object data;
            switch (Type)
            {
                case BodyType.None:
                    {
                        data = (object)null;
                    }
                    break;
                case BodyType.Binary:
                    {
                        byte[] bytes = new byte[count];
                        Array.Copy(segment, index, bytes, 0, count);
                        data = bytes;
                    }
                    break;
                case BodyType.Boolean:
                    {
                        data = count > 0 && segment[index] != 0;
                    }
                    break;
                case BodyType.Integer:
                    {
                        if (BitConverter.IsLittleEndian)
                        {
                            Array.Reverse(segment, index, count);
                        }
                        if (count == 2)
                        {
                            data = BitConverter.ToInt16(segment, index);
                        }
                        else if (count == 4)
                        {
                            data = BitConverter.ToInt32(segment, index);
                        }
                        else if (count == 8)
                        {
                            data = BitConverter.ToInt64(segment, index);
                        }
                        else
                        {
                            string s = string.Format("wrong Integer len {0}", count);
                            throw new Exception(s);
                        }
                    }
                    break;
                case BodyType.UInteger:
                    {
                        if (BitConverter.IsLittleEndian)
                        {
                            Array.Reverse(segment, index, count);
                        }
                        if (count == 8)
                        {
                            data = BitConverter.ToUInt64(segment, index);
                        }
                        else if (count == 4)
                        {
                            data = BitConverter.ToUInt32(segment, index);
                        }
                        else if (count == 2)
                        {
                            data = BitConverter.ToUInt16(segment, index);
                        }
                        else if (count == 1)
                        {
                            data = segment[index];
                        }
                        else
                        {
                            string s = string.Format("wrong Integer len {0}", count);
                            throw new Exception(s);
                        }
                    }
                    break;
                case BodyType.String:
                    {
                        data = Encoding.UTF8.GetString(segment, index, count);
                    }
                    break;
                case BodyType.ProtoBuf:
                    {
                        if (mCmdHelper == null)
                        {
                            throw new Exception("no CmdProtoBufTypeGetter given");
                        }
                        var type = Response ? mCmdHelper.GetCmdHandler(Cmd).Out : mCmdHelper.GetCmdHandler(Cmd).In;
                        data = PB.Deserialize(type, segment, index, count);
                    }
                    break;
                default:
                    {
                        throw new Exception("invalid body type " + Type);
                    }
                    //break;
            }
            Data = data;
        }
    }
}
